﻿using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using Aeon.Emulator;
using Aeon.Emulator.DebugSupport;

namespace Aeon.Presentation
{
    /// <summary>
    /// Interaction logic for EmulatorDisplay.xaml
    /// </summary>
    public sealed partial class EmulatorDisplay : ContentControl
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the EmulatorDisplay class.
        /// </summary>
        public EmulatorDisplay()
        {
            updateHandler = new EventHandler(GraphicalUpdate);
            this.resumeCommand = new SimpleCommand(() => this.EmulatorState == EmulatorState.Paused, () => { this.EmulatorHost.Run(); });
            this.pauseCommand = new SimpleCommand(() => this.EmulatorState == EmulatorState.Running, () => { this.EmulatorHost.Pause(); });

            InitializeComponent();
        }
        #endregion

        #region Public Events
        /// <summary>
        /// Occurs when the emulator's state has changed.
        /// </summary>
        public event RoutedEventHandler EmulatorStateChanged
        {
            add { AddHandler(EmulatorStateChangedEvent, value); }
            remove { RemoveHandler(EmulatorStateChangedEvent, value); }
        }
        /// <summary>
        /// Occurs when an error in emulation causes the emulator to halt.
        /// </summary>
        public event EmulationErrorRoutedEventHandler EmulationError
        {
            add { AddHandler(EmulationErrorEvent, value); }
            remove { RemoveHandler(EmulationErrorEvent, value); }
        }
        /// <summary>
        /// Occurs when the current process has changed.
        /// </summary>
        public event RoutedEventHandler CurrentProcessChanged
        {
            add { AddHandler(CurrentProcessChangedEvent, value); }
            remove { RemoveHandler(CurrentProcessChangedEvent, value); }
        }
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets or sets the emulator to display.
        /// </summary>
        public EmulatorHost EmulatorHost
        {
            get
            {
                if(this.emulator == null)
                {
                    this.emulator = new EmulatorHost(this.instructionLog);
                    this.emulator.EventSynchronizer = new WpfSynchronizer(this.Dispatcher);
                    this.emulator.VideoModeChanged += this.HandleModeChange;
                    this.emulator.StateChanged += this.emulator_StateChanged;
                    this.emulator.MouseVisibilityChanged += this.emulator_MouseVisibilityChanged;
                    this.emulator.MouseMove += this.emulator_MouseMove;
                    this.emulator.Error += this.emulator_Error;
                    emulator.CurrentProcessChanged += this.emulator_CurrentProcessChanged;

                    this.emulator.EmulationSpeed = this.EmulationSpeed;
                    this.timer.Start();
                    InitializePresenter();
                }

                return this.emulator;
            }
        }
        /// <summary>
        /// Gets the current state of the emulator.  This is a dependency property.
        /// </summary>
        public EmulatorState EmulatorState
        {
            get { return (EmulatorState)GetValue(EmulatorStateProperty); }
        }
        /// <summary>
        /// Gets or sets a value indicating whether the correct aspect ratio is maintained. This is a dependency property.
        /// </summary>
        public bool IsAspectRatioLocked
        {
            get { return (bool)GetValue(IsAspectRatioLockedProperty); }
            set { SetValue(IsAspectRatioLockedProperty, value); }
        }
        /// <summary>
        /// Gets or sets a value indicating the type of mouse input provided.  This is a dependency property.
        /// </summary>
        public MouseInputMode MouseInputMode
        {
            get { return (MouseInputMode)GetValue(MouseInputModeProperty); }
            set { SetValue(MouseInputModeProperty, value); }
        }
        /// <summary>
        /// Gets a value indicating whether the emulator has captured mouse input.  This is a dependency property.
        /// </summary>
        public bool IsMouseCursorCaptured
        {
            get { return (bool)GetValue(IsMouseCursorCapturedProperty); }
        }
        /// <summary>
        /// Gets or sets the emulation speed.  This is a dependency property.
        /// </summary>
        public int EmulationSpeed
        {
            get { return (int)GetValue(EmulationSpeedProperty); }
            set { SetValue(EmulationSpeedProperty, value); }
        }
        /// <summary>
        /// Gets the BitmapSource used for rendering the output display.
        /// </summary>
        public BitmapSource DisplayBitmap
        {
            get
            {
                var bmp = fastBitmap;
                if(bmp != null)
                    return bmp.InteropBitmap;
                else
                    return null;
            }
        }
        /// <summary>
        /// Gets information about the current process. This is a dependency property.
        /// </summary>
        public Aeon.Emulator.Dos.DosProcess CurrentProcess
        {
            get { return (Aeon.Emulator.Dos.DosProcess)GetValue(CurrentProcessProperty); }
        }
        /// <summary>
        /// Gets the command used to resume the emulator from a paused state.
        /// </summary>
        public ICommand ResumeCommand
        {
            get { return this.resumeCommand; }
        }
        /// <summary>
        /// Gets the command used to pause the emulator.
        /// </summary>
        public ICommand PauseCommand
        {
            get { return this.pauseCommand; }
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Disposes the current emulator and returns the control to its default state.
        /// </summary>
        public void ResetEmulator()
        {
            if(this.emulator != null)
            {
                this.emulator.VideoModeChanged -= this.HandleModeChange;
                this.emulator.StateChanged -= this.emulator_StateChanged;
                this.emulator.MouseVisibilityChanged -= this.emulator_MouseVisibilityChanged;
                this.emulator.MouseMove -= this.emulator_MouseMove;
                this.emulator.Error -= this.emulator_Error;
                this.emulator.CurrentProcessChanged -= this.emulator_CurrentProcessChanged;
                this.mouseImage.Visibility = Visibility.Collapsed;
                this.cursorRectangle.Visibility = Visibility.Collapsed;
                this.timer.Stop();

                this.emulator.Dispose();
                this.emulator = null;
            }
        }
        /// <summary>
        /// Sets the instruction log which will be used to record instructions.
        /// </summary>
        /// <param name="log">Instruction log used to record instruction. Null disables logging.</param>
        [CLSCompliant(false)]
        public void SetInstructionLog(InstructionLog log)
        {
            this.instructionLog = log;
        }
        #endregion

        #region Protected Methods
        protected override void OnInitialized(EventArgs e)
        {
            this.timer = new DispatcherTimer(TimeSpan.FromSeconds(1.0 / 60.0), DispatcherPriority.Render, updateHandler, this.Dispatcher);
            base.OnInitialized(e);
        }
        protected override void OnKeyDown(KeyEventArgs e)
        {
            if(emulator != null && emulator.State == EmulatorState.Running)
            {
                if(e.Key == Key.F12 && (e.KeyboardDevice.Modifiers & ModifierKeys.Control) != 0)
                {
                    isMouseCaptured = false;
                    SetValue(IsMouseCursorCapturedPropertyKey, false);
                }
                else
                {
                    Keys key;
                    if(e.Key != Key.System)
                        key = e.Key.ToEmulatorKey();
                    else
                        key = e.SystemKey.ToEmulatorKey();

                    if(key != Keys.Null)
                        emulator.PressKey(key);
                }

                e.Handled = true;
            }
            base.OnKeyDown(e);
        }
        protected override void OnKeyUp(KeyEventArgs e)
        {
            if(emulator != null && emulator.State == EmulatorState.Running)
            {
                Keys key;
                if(e.Key != Key.System)
                    key = e.Key.ToEmulatorKey();
                else
                    key = e.SystemKey.ToEmulatorKey();

                if(key != Keys.Null)
                    emulator.ReleaseKey(key);

                e.Handled = true;
            }
            base.OnKeyUp(e);
        }
        protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
        {
            isMouseCaptured = false;
            SetValue(IsMouseCursorCapturedPropertyKey, false);
            if(emulator != null && emulator.State == EmulatorState.Running)
                emulator.ReleaseAllKeys();

            base.OnLostKeyboardFocus(e);
        }
        protected override void OnContentChanged(object oldContent, object newContent)
        {
            if(oldContent != null)
                throw new InvalidOperationException("EmulatorDisplay does not support content.");

            base.OnContentChanged(oldContent, newContent);
        }
        #endregion
        
        #region Private Methods
        /// <summary>
        /// Method invoked by the dispatch timer to redraw the screen.
        /// </summary>
        /// <param name="sender">Source of event.</param>
        /// <param name="e">Unused EventArgs object.</param>
        private void GraphicalUpdate(object sender, EventArgs e)
        {
            if(emulator != null)
            {
                if(presenter != null)
                    presenter.Update();

                if(fastBitmap != null)
                    fastBitmap.Invalidate();

                if(this.emulator.VirtualMachine.IsCursorVisible)
                {
                    cursorBlink = (cursorBlink + 1) % 16;
                    if(cursorBlink == 8)
                        this.cursorRectangle.Visibility = Visibility.Visible;
                    else if(cursorBlink == 0)
                        this.cursorRectangle.Visibility = Visibility.Collapsed;

                    var cursorPosition = this.emulator.VirtualMachine.CursorPosition;
                    if(cursorPosition != this.cursorPosition)
                    {
                        this.cursorPosition = cursorPosition;
                        Canvas.SetLeft(this.cursorRectangle, cursorPosition.X * 8);
                        Canvas.SetTop(this.cursorRectangle, (cursorPosition.Y * emulator.VirtualMachine.VideoMode.FontHeight) + emulator.VirtualMachine.VideoMode.FontHeight - 2);
                    }
                }
                else if(this.cursorRectangle.Visibility == Visibility.Visible)
                    this.cursorRectangle.Visibility = Visibility.Collapsed;
            }
        }
        /// <summary>
        /// Method invoked when the emulated video mode changes.
        /// </summary>
        /// <param name="sender">Source of event.</param>
        /// <param name="e">Unused EventArgs object.</param>
        private void HandleModeChange(object sender, EventArgs e)
        {
            this.InitializePresenter();
        }
        /// <summary>
        /// Initializes the presenter and bitmap after a video mode change.
        /// </summary>
        private void InitializePresenter()
        {
            if(emulator != null)
            {
                var videoMode = emulator.VirtualMachine.VideoMode;
                fastBitmap = new FastBitmap(videoMode.PixelWidth, videoMode.PixelHeight);

                if(videoMode.VideoModeType == Aeon.Emulator.Video.VideoModeType.Text)
                {
                    presenter = new TextPresenter(fastBitmap.PixelBuffer, videoMode);
                }
                else
                {
                    switch(videoMode.BitsPerPixel)
                    {
                    case 4:
                        presenter = new GraphicsPresenter4(fastBitmap.PixelBuffer, videoMode);
                        break;

                    case 8:
                        if(!videoMode.IsPlanar)
                            presenter = new GraphicsPresenter8(fastBitmap.PixelBuffer, videoMode);
                        else
                            presenter = new GraphicsPresenterX(fastBitmap.PixelBuffer, videoMode);
                        break;

                    case 16:
                        presenter = new GraphicsPresenter16(fastBitmap.PixelBuffer, videoMode);
                        break;
                    }
                }

                displayImage.Source = fastBitmap.InteropBitmap;
                displayImage.Width = videoMode.PixelWidth;
                displayImage.Height = videoMode.PixelHeight;
                displayArea.Width = videoMode.PixelWidth;
                displayArea.Height = videoMode.PixelHeight;

                centerPoint.X = videoMode.PixelWidth / 2;
                centerPoint.Y = videoMode.PixelHeight / 2;
            }
            else
            {
                presenter = null;
                fastBitmap = null;
                displayImage.Source = null;
            }
        }
        /// <summary>
        /// Adjusts the position of the emulated mouse cursor.
        /// </summary>
        /// <param name="x">Horizonal coordinate of mouse cursor.</param>
        /// <param name="y">Vertical coordinate of mouse cursor.</param>
        private void MoveMouseCursor(int x, int y)
        {
            Canvas.SetLeft(mouseImage, x);
            Canvas.SetTop(mouseImage, y);
        }
        #endregion

        #region Private Static Methods
        private static void OnEmulationSpeedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var obj = (EmulatorDisplay)d;
            if(obj.emulator != null)
                obj.emulator.EmulationSpeed = (int)e.NewValue;
        }
        private static bool EmulationSpeedChangedValidate(object value)
        {
            int n = (int)value;
            return n >= EmulatorHost.MinimumSpeed && n <= EmulatorHost.MaximumSpeed;
        }
        private static void OnIsAspectRatioLockedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var obj = (EmulatorDisplay)d;
            bool value = (bool)e.NewValue;
            obj.outerViewbox.Stretch = value ? Stretch.Uniform : Stretch.Fill;
        }
        #endregion

        #region Event Handlers
        private void emulator_StateChanged(object sender, EventArgs e)
        {
            if(emulator != null)
            {
                SetValue(EmulatorStatePropertyKey, emulator.State);
                this.resumeCommand.UpdateState();
                this.pauseCommand.UpdateState();
                RaiseEvent(new RoutedEventArgs(EmulatorStateChangedEvent));
            }
        }
        private void emulator_MouseVisibilityChanged(object sender, EventArgs e)
        {
            this.mouseImage.Visibility = emulator.VirtualMachine.IsMouseVisible ? Visibility.Visible : Visibility.Collapsed;
        }
        private void emulator_MouseMove(object sender, MouseMoveEventArgs e)
        {
            MoveMouseCursor(e.X, e.Y);
        }
        private void emulator_Error(object sender, ErrorEventArgs e)
        {
            RaiseEvent(new EmulationErrorRoutedEventArgs(EmulationErrorEvent, e.Message));
        }
        private void emulator_CurrentProcessChanged(object sender, EventArgs e)
        {
            if(emulator != null)
                SetValue(CurrentProcessPropertyKey, emulator.VirtualMachine.CurrentProcess);
            else
                SetValue(CurrentProcessPropertyKey, null);

            RaiseEvent(new RoutedEventArgs(CurrentProcessChangedEvent));
        }
        private void displayImage_MouseDown(object sender, MouseButtonEventArgs e)
        {
            if(emulator != null && emulator.State == EmulatorState.Running)
            {
                if(!isMouseCaptured && this.MouseInputMode == MouseInputMode.Relative)
                {
                    SetValue(IsMouseCursorCapturedPropertyKey, true);
                    mouseJustCaptured = true;
                    isMouseCaptured = true;

                    centerPoint.X = displayImage.Width / 2;
                    centerPoint.Y = displayImage.Height / 2;
                    return;
                }

                var button = e.ChangedButton.ToEmulatorButtons();
                if(button != MouseButtons.None)
                {
                    var mouseEvent = new MouseButtonDownEvent(button);
                    emulator.MouseEvent(mouseEvent);
                }
            }
        }
        private void displayImage_MouseUp(object sender, MouseButtonEventArgs e)
        {
            if(emulator != null && emulator.State == EmulatorState.Running)
            {
                if(mouseJustCaptured)
                {
                    mouseJustCaptured = false;
                    return;
                }

                var button = e.ChangedButton.ToEmulatorButtons();
                if(button != MouseButtons.None)
                {
                    var mouseEvent = new MouseButtonUpEvent(button);
                    emulator.MouseEvent(mouseEvent);
                }
            }
        }
        private void displayImage_MouseMove(object sender, MouseEventArgs e)
        {
            if(emulator != null && emulator.State == EmulatorState.Running)
            {
                if(this.MouseInputMode == MouseInputMode.Absolute)
                {
                    var pos = e.GetPosition(displayImage);
                    emulator.MouseEvent(new MouseMoveAbsoluteEvent((int)pos.X, (int)pos.Y));
                }
                else if(isMouseCaptured)
                {
                    var deltaPos = Mouse.GetPosition(displayImage);

                    int dx = (int)(deltaPos.X - centerPoint.X);
                    int dy = (int)(deltaPos.Y - centerPoint.Y);

                    if(dx != 0 || dy != 0)
                    {
                        emulator.MouseEvent(new MouseMoveRelativeEvent(dx, dy));
                        var p = displayImage.PointToScreen(centerPoint);
                        SafeNativeMethods.SetCursorPos((int)p.X, (int)p.Y);
                    }
                }
            }
        }
        #endregion

        #region Private Fields
        private Presenter presenter;
        private EmulatorHost emulator;
        private FastBitmap fastBitmap;
        private bool mouseJustCaptured;
        private bool isMouseCaptured;
        private Point centerPoint;
        private DispatcherTimer timer;
        private EventHandler updateHandler;
        private int cursorBlink;
        private Aeon.Emulator.Video.Point cursorPosition = new Aeon.Emulator.Video.Point(0, 1);
        private SimpleCommand resumeCommand;
        private SimpleCommand pauseCommand;
        private InstructionLog instructionLog;
        #endregion

        #region Private Static Fields
        private static readonly DependencyPropertyKey EmulatorStatePropertyKey = DependencyProperty.RegisterReadOnly("EmulatorState", typeof(EmulatorState), typeof(EmulatorDisplay), new PropertyMetadata(EmulatorState.NoProgram));
        private static readonly DependencyPropertyKey IsMouseCursorCapturedPropertyKey = DependencyProperty.RegisterReadOnly("IsMouseCursorCaptured", typeof(bool), typeof(EmulatorDisplay), new PropertyMetadata(false));
        private static readonly DependencyPropertyKey CurrentProcessPropertyKey = DependencyProperty.RegisterReadOnly("CurrentProcess", typeof(Aeon.Emulator.Dos.DosProcess), typeof(EmulatorDisplay), new PropertyMetadata(null));
        #endregion

        #region Public Static Fields
        public static readonly DependencyProperty EmulatorStateProperty = EmulatorStatePropertyKey.DependencyProperty;
        public static readonly DependencyProperty CurrentProcessProperty = CurrentProcessPropertyKey.DependencyProperty;
        public static readonly DependencyProperty MouseInputModeProperty = DependencyProperty.Register("MouseInputMode", typeof(MouseInputMode), typeof(EmulatorDisplay), new PropertyMetadata(MouseInputMode.Relative));
        public static readonly DependencyProperty IsMouseCursorCapturedProperty = IsMouseCursorCapturedPropertyKey.DependencyProperty;
        public static readonly DependencyProperty EmulationSpeedProperty = DependencyProperty.Register("EmulationSpeed", typeof(int), typeof(EmulatorDisplay), new PropertyMetadata(18, OnEmulationSpeedChanged), EmulationSpeedChangedValidate);
        public static readonly DependencyProperty IsAspectRatioLockedProperty = DependencyProperty.Register("IsAspectRatioLocked", typeof(bool), typeof(EmulatorDisplay), new PropertyMetadata(true, OnIsAspectRatioLockedChanged));
        public static readonly RoutedEvent EmulatorStateChangedEvent = EventManager.RegisterRoutedEvent("EmulatorStateChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(EmulatorDisplay));
        public static readonly RoutedEvent EmulationErrorEvent = EventManager.RegisterRoutedEvent("EmulationError", RoutingStrategy.Bubble, typeof(EmulationErrorRoutedEventHandler), typeof(EmulatorDisplay));
        public static readonly RoutedEvent CurrentProcessChangedEvent = EventManager.RegisterRoutedEvent("CurrentProcessChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(EmulatorDisplay));
        #endregion
    }

    /// <summary>
    /// Specifies the type of mouse input provided to the emulator.
    /// </summary>
    public enum MouseInputMode
    {
        /// <summary>
        /// Mouse movement is captured and reported to the emulator in relative units.
        /// </summary>
        Relative,
        /// <summary>
        /// Mouse movement is not captured and is reported to the emulator at absolute coordinates.
        /// </summary>
        Absolute
    }

    public delegate void EmulationErrorRoutedEventHandler(object sender, EmulationErrorRoutedEventArgs e);

    /// <summary>
    /// Contains information about an emulation error.
    /// </summary>
    public class EmulationErrorRoutedEventArgs : RoutedEventArgs
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the EmulationErrorRoutedEventArgs class.
        /// </summary>
        /// <param name="routedEvent">RoutedEvent identifier.</param>
        /// <param name="message">Message describing the error.</param>
        public EmulationErrorRoutedEventArgs(RoutedEvent routedEvent, string message)
            : base(routedEvent)
        {
            this.message = message;
        }
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets a message which describes the error.
        /// </summary>
        public string Message
        {
            get { return message; }
        }
        #endregion

        #region Private Fields
        private readonly string message;
        #endregion
    }
}
